package com.example.demo.searcher;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: ling
 * Date: 2023-02-04
 * Time: 12:17
 */

import com.example.demo.model.DocInfo;
import com.example.demo.model.Weight;
import com.example.demo.model.WordCnt;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.ansj.domain.Term;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 通过这个类在内存中构造出索引
 */
@Service
public class Index {
    //作为锁对象
    private Object locker1 = new Object();
    private Object locker2 = new Object();
    //索引制作完成后，写入磁盘时，索引数据保存的文件路径
    private static final String INDEX_PATH = "D:/doc_searcher_index/";

    //正排索引，数组下标表示文档ID也就是docId
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    //使用哈希表来作为倒排索引的数据结构，key表示词，val表示一组和这个词相关的文章
    private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();

    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 根据文档id查正排
     * @param docId
     * @return
     */
    public DocInfo getDocInfo(int docId) {
        return forwardIndex.get(docId);
    }

    /**
     * 根据查询词查倒排，返回一个倒排拉链
     * @param term
     * @return
     */
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    /**
     * 往索引中添加文档，正排索引的倒排索引都要添加
     * @param title
     * @param url
     * @param content
     */
    public void addDoc(String title,String url,String content) {
        //往正排索引添加文档
        DocInfo docInfo = buildForward(title,url,content);
        //往倒排索引添加文档
        buildInverted(docInfo);
    }

    /**
     * 将索引数据保存到本地磁盘中
     */
    public void save() {
        long beg = System.currentTimeMillis();
        //使用两个文件分别保存正排索引和倒排索引的数据的数据
        File indexPathFile = new File(INDEX_PATH);
        //1.先判定一下索引目录是否存在，如果不存在，则创建
        if(!indexPathFile.exists()) {
            indexPathFile.mkdirs();
        }
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            objectMapper.writeValue(forwardIndexFile,forwardIndex);
            objectMapper.writeValue(invertedIndexFile,invertedIndex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("索引保存完成，消耗时间："+ (end-beg));
    }

    /**
     * 将索引数据从本地磁盘中加载到内存中
     */
    public void load() {
        long beg = System.currentTimeMillis();
        //1.先设置一下加载索引的路径
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            //从文件中读取json格式数据，然后转成forwardIndex的数据结构
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
            //从文件中读取json格式数据，然后转invertedIndex的数据结构
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("索引加载消耗时间:"+(end-beg));
    }

    /**
     * 往正排索引中添加新文档（构建正排索引）
     * @param title
     * @param url
     * @param content
     * @return
     */
    private DocInfo buildForward(String title,String url,String content) {
        //1.新建一个DocInfo对象，将对应的属性填入
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        //关于这里为什么要进行加锁操作，后面会详细说明
        synchronized (locker1) {
            docInfo.setDocId(forwardIndex.size());
            //2.往forwardIndex中add这个docInfo对象
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

    /**
     * 往倒排索引中添加新文档（构建倒排索引）
     * @param docInfo
     */
    private void buildInverted(DocInfo docInfo) {
        //这个hashmap用来统计词频
        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();
        //1.针对文档标题进行分词
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        //2.遍历分词结果，统计每个词出现的次数
        for (Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(term);
            //先判断term是否存在
            if(wordCnt==null) {
                //若不存在，新建一个键值对，将titleCount设为1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount=1;
                newWordCnt.contentCount=0;
                wordCntHashMap.put(word,newWordCnt);
            } else {
                //若存在则将titleCount加1
                wordCnt.titleCount+=1;
            }
        }
        //3.针对文档内容进行分词
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        //4.遍历分词结果，统计每个词出现的次数
        for(Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(term);
            //先判断term是否存在
            if(wordCnt==null) {
                //若不存在，新建一个键值对，将titleCount设为1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(word,newWordCnt);
            } else {
                //若存在，则将contentCount加1
                wordCnt.contentCount+=1;
            }
        }
        //5.把上面结果汇总到一个hashmap只能（最终权重计算：titleCount*10+contentCount）
        //6.遍历刚才这个hashmap，依次来更新倒排索引中的结构
        for(Map.Entry<String,WordCnt> entry : wordCntHashMap.entrySet()) {
            synchronized (locker2) {
                //先根据这里的词去倒排索引中查一查
                //倒排拉链
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                if(invertedList==null) {
                    //若为空则插入新的键值对
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    //把新的文档构造成weight对象，插入进入
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().titleCount*10 + entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    invertedIndex.put(entry.getKey(),newInvertedList);
                } else {
                    //若非空，就把当前文档构造成一个weight对象，插入到倒排拉链中
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    //权重计算
                    weight.setWeight(entry.getValue().titleCount*10 + entry.getValue().contentCount);
                    invertedList.add(weight);
                }

            }
        }

    }
}
